/*

 * Licensed to the Apache Software Foundation (ASF) under one

 * or more contributor license agreements.  See the NOTICE file

 * distributed with this work for additional information

 * regarding copyright ownership.  The ASF licenses this file

 * to you under the Apache License, Version 2.0 (the

 * "License"); you may not use this file except in compliance

 * with the License.  You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package com.bff.gaia.unified.sdk.util;



import com.bff.gaia.unified.sdk.transforms.Combine.CombineFn;

import com.bff.gaia.unified.sdk.transforms.DoFn;

import com.bff.gaia.unified.sdk.transforms.PTransform;

import com.bff.gaia.unified.vendor.guava.com.google.common.annotations.VisibleForTesting;

import com.bff.gaia.unified.vendor.guava.com.google.common.base.Joiner;



import java.util.regex.Matcher;

import java.util.regex.Pattern;



import static com.bff.gaia.unified.vendor.guava.com.google.common.base.Preconditions.checkArgument;



/** Helpers for extracting the name of objects and classes. */

public class NameUtils {



  /** Classes may implement this interface to change how names are generated for their instances. */

  public interface NameOverride {

    /** Return the name to use for this instance. */

    String getNameOverride();

  }



  private static final String[] STANDARD_NAME_SUFFIXES = new String[] {"DoFn", "CombineFn", "Fn"};



  /**

   * Pattern to match a non-anonymous inner class. Eg, matches "Foo$Bar", or even "Foo$1$Bar", but

   * not "Foo$1" or "Foo$1$2".

   */

  private static final Pattern NAMED_INNER_CLASS = Pattern.compile(".+\\$(?<INNER>[^0-9].*)");



  private static final String ANONYMOUS_CLASS_REGEX = "\\$[0-9]+\\$";



  private static String approximateSimpleName(Class<?> clazz, boolean dropOuterClassNames) {

    checkArgument(!clazz.isAnonymousClass(), "Attempted to get simple name of anonymous class");

    return approximateSimpleName(clazz.getName(), dropOuterClassNames);

  }



  @VisibleForTesting

  static String approximateSimpleName(String fullName, boolean dropOuterClassNames) {

    String shortName = fullName.substring(fullName.lastIndexOf('.') + 1);



    // Drop common suffixes for each named component.

    String[] names = shortName.split("\\$");

    for (int i = 0; i < names.length; i++) {

      names[i] = simplifyNameComponent(names[i]);

    }

    shortName = Joiner.on('$').join(names);



    if (dropOuterClassNames) {

      // Simplify inner class name by dropping outer class prefixes.

      Matcher m = NAMED_INNER_CLASS.matcher(shortName);

      if (m.matches()) {

        shortName = m.group("INNER");

      }

    } else {

      // Dropping anonymous outer classes

      shortName = shortName.replaceAll(ANONYMOUS_CLASS_REGEX, ".");

      shortName = shortName.replaceAll("\\$", ".");

    }

    return shortName;

  }



  private static String simplifyNameComponent(String name) {

    for (String suffix : STANDARD_NAME_SUFFIXES) {

      if (name.endsWith(suffix) && name.length() > suffix.length()) {

        return name.substring(0, name.length() - suffix.length());

      }

    }

    return name;

  }



  /**

   * As {@link #approximateSimpleName(Object, String)} but returning {@code "Anonymous"} when {@code

   * object} is an instance of anonymous class.

   */

  public static String approximateSimpleName(Object object) {

    return approximateSimpleName(object, "Anonymous");

  }



  /**

   * Returns a simple name describing a class that is being used as a function (eg., a {@link DoFn}

   * or {@link CombineFn}, etc.).

   *

   * <p>Note: this is non-invertible - the name may be simplified to an extent that it cannot be

   * mapped back to the original class.

   *

   * <p>This can be used to generate human-readable names. It removes the package and outer classes

   * from the name, and removes common suffixes.

   *

   * <p>If the object is an instanceof {@link NameOverride}, the result of {@link

   * NameOverride#getNameOverride()} is returned. This allows classes that act as wrappers to

   * override the handling of names by delegating to the objects they wrap.

   *

   * <p>If the class is anonymous, the string {@code anonymousValue} is returned.

   *

   * <p>Examples:

   *

   * <ul>

   *   <li>{@code some.package.Word.SummaryDoFn} becomes "Summary"

   *   <li>{@code another.package.PairingFn} becomes "Pairing"

   * </ul>

   */

  public static String approximateSimpleName(Object object, String anonymousValue) {

    if (object instanceof NameOverride) {

      return ((NameOverride) object).getNameOverride();

    }



    Class<?> clazz;

    if (object instanceof Class) {

      clazz = (Class<?>) object;

    } else {

      clazz = object.getClass();

    }

    if (clazz.isAnonymousClass()) {

      return anonymousValue;

    }



    return approximateSimpleName(clazz, /* dropOuterClassNames */ true);

  }



  /**

   * Returns a name for a PTransform class.

   *

   * <p>This can be used to generate human-readable transform names. It removes the package from the

   * name, and removes common suffixes.

   *

   * <p>It is different than approximateSimpleName:

   *

   * <ul>

   *   <li>1. It keeps the outer classes names.

   *   <li>2. It removes the common transform inner class: "Bound".

   *   <li>3. For classes generated by AutoValue, whose names start with AutoValue_, it delegates to

   *       the (parent) class declared in the user's source code.

   * </ul>

   *

   * <p>Examples:

   *

   * <ul>

   *   <li>{@code some.package.Word.Summary} becomes "Word.Summary"

   *   <li>{@code another.package.Pairing.Bound} becomes "Pairing"

   * </ul>

   */

  public static String approximatePTransformName(Class<?> clazz) {

    checkArgument(PTransform.class.isAssignableFrom(clazz));

    if (clazz.getSimpleName().startsWith("AutoValue_")) {

      return approximatePTransformName(clazz.getSuperclass());

    }

    return approximateSimpleName(clazz, /* dropOuterClassNames */ false)

        .replaceFirst("\\.Bound$", "");

  }

}